<?php

namespace App\Utils\Modbus;

use Exception;
use ModbusTcpClient\Network\BinaryStreamConnection;
use ModbusTcpClient\Network\StreamCreator;
use ModbusTcpClient\Packet\ModbusFunction\ReadHoldingRegistersRequest;
use ModbusTcpClient\Packet\ModbusFunction\ReadInputRegistersRequest;
use ModbusTcpClient\Packet\RtuConverter;
use ModbusTcpClient\Utils\Packet;

class ModbusRtu
{
    protected BinaryStreamConnection $connection;
    protected string $host;
    protected int $port;
    protected string $device;
    public function __construct(string $deviceURI="COM3",$timeout=0.5)
    {
        $this->connection = BinaryStreamConnection::getBuilder()
            ->setUri($deviceURI)
            ->setProtocol(StreamCreator::TYPE_SERIAL)
            ->setConnectTimeoutSec(1.5) // timeout when establishing connection to the server
            ->setWriteTimeoutSec($timeout) // timeout when writing/sending packet to the server
            ->setReadTimeoutSec(0.3) // timeout when waiting response from server
            ->setIsCompleteCallback(function ($binaryData, $streamIndex) {
                return Packet::isCompleteLengthRTU($binaryData);
            })
            ->setDelayRead(100_000)
            ->build();
        $this->device=$deviceURI;
    }
    public function getValue(int $address, int $quantity=1,int $slaveId=1): array|string
    {
        $packet = new ReadHoldingRegistersRequest($address, $quantity, $slaveId);
        //$packet = new ReadInputRegistersRequest($address, $quantity, $slaveId);
        $rtuPacket = RtuConverter::toRtu($packet);

        try {
            $binaryData = $this->connection->connect()->sendAndReceive($rtuPacket);
            echo 'RTU Binary received (in hex):   ' . unpack('H*', $binaryData)[1] . PHP_EOL;
            $response = RtuConverter::fromRtuOrThrow($binaryData);
            $response = $response->withStartAddress($address);
            $result=[];
            foreach ($response as $address => $word) {
                $doubleWord = isset($response[$address + 1]) ? $response->getDoubleWordAt($address) : null;
                $quadWord = null;
                if (isset($response[$address + 3])) {
                    $quadWord = $response->getQuadWordAt($address);
                    try {
                        $UInt64 = $quadWord->getUInt64(); // some data can not be converted to unsigned 64bit int due PHP memory limitations
                    } catch (Exception $e) {
                        $UInt64 = '-';
                    }
                    try {
                        $Int64 = $quadWord->getInt64();
                    } catch (Exception $e) {
                        $Int64 = '-';
                    }
                    try {
                        $double = $quadWord->getDouble();
                    } catch (Exception $e) {
                        $double = '-';
                    }

                }
                $highByteAsInt = $word->getHighByteAsInt();
                $lowByteAsInt = $word->getLowByteAsInt();
                $result[$address] = [
                    'highByte' => '0x' . str_pad(dechex($highByteAsInt), 2, '0', STR_PAD_LEFT) . ' / ' . $highByteAsInt . ' / "&#' . $highByteAsInt . ';"',
                    'lowByte' => '0x' . str_pad(dechex($lowByteAsInt), 2, '0', STR_PAD_LEFT) . ' / ' . $lowByteAsInt . ' / "&#' . $lowByteAsInt . ';"',
                    'highByteBits' => sprintf('%08d', decbin($highByteAsInt)),
                    'lowByteBits' => sprintf('%08d', decbin($lowByteAsInt)),
                    'int16' => $word->getInt16(),
                    'UInt16' => $word->getUInt16(),
                    'int32' => $doubleWord ? $doubleWord->getInt32() : null,
                    'UInt32' => $doubleWord ? $doubleWord->getUInt32() : null,
                    'float' => $doubleWord ? $doubleWord->getFloat() : null,
                    'double' => $quadWord ? $double : null,
                    'Int64' => $quadWord ? $Int64 : null,
                    'UInt64' => $quadWord ? $UInt64 : null,
                ];
            }
            return $result;
        } catch (Exception $exception) {
            return $exception->getMessage() ;
        } finally {
            $this->connection->close();
        }
    }

    /**
     * 读取串口里面的数据
     * @param int $startAddress
     * @param int $quantity
     * @param int $slaveId
     * @return array|string
     */
    public function getReadSerial(int $startAddress ,int $quantity,int $slaveId): array|string
    {
        $tcpPacket = new ReadInputRegistersRequest($startAddress, $quantity, $slaveId);
        $rtuPacket = RtuConverter::toRtu($tcpPacket);
        try {
            $device = $this->device??"/dev/ttyUSB0";
            $fd = fopen($device, 'w+b');
            $sttyModes = implode(' ', [
                'cs8', // enable character size 8 bits
                '9600', // enable baud rate 9600
                '-cstopb', // 1 stop bit
                '-parenb', // parity none

                '-icanon', // disable enable special characters: erase, kill, werase, rprnt
                'min 0', // with -icanon, set N characters minimum for a completed read
                'ignbrk', // enable ignore break characters
                '-brkint', // disable breaks cause an interrupt signal
                '-icrnl', // disable translate carriage return to newline
                '-imaxbel', // disable beep and do not flush a full input buffer on a character
                '-opost', // disable postprocess output
                '-onlcr', // disable translate newline to carriage return-newline
                '-isig', // disable interrupt, quit, and suspend special characters
                '-iexten', // disable non-POSIX special characters
                '-echo', // disable echo input characters
                '-echoe', // disable echo erase characters as backspace-space-backspace
                '-echok', // disable echo a newline after a kill character
                '-echoctl', // disable same as [-]ctlecho
                '-echoke', // disable kill all line by obeying the echoprt and echoe settings
                '-noflsh', // disable flushing after interrupt and quit special characters
                '-ixon', // disable XON/XOFF flow control
                '-crtscts', // disable RTS/CTS handshaking
            ]);
            $sttyResult = exec("stty -F {$device} {$sttyModes}");
            if ($sttyResult === false) {
               return 'stty command failed';
            }
            fwrite($fd, $rtuPacket);
            fflush($fd);
            $binaryData = '';
            $start = microtime(true);
            do {
                usleep(5000);
                $binaryData = fread($fd, 255);
            } while ($binaryData === '');
            $end = (microtime(true) - $start) * 1000;
            $response = RtuConverter::fromRtu($binaryData)->withStartAddress($startAddress);
            $data['request_message']=unpack('H*', $rtuPacket)[1];
            $data['response_message']= $response->toHex();
            $data['data']= $response->getWordAt(1)->getInt16();
            $data['time']= $end . ' ms';
            return $data;
        }catch (\Exception $exception){
            return $exception->getMessage();
        }finally {
            fclose($fd);
        }

    }
}